package xyz.scootaloo.kami.server.verticle

import io.vertx.core.CompositeFuture
import io.vertx.core.DeploymentOptions
import io.vertx.core.Vertx
import io.vertx.kotlin.core.deploymentOptionsOf
import io.vertx.kotlin.coroutines.CoroutineVerticle
import io.vertx.kotlin.coroutines.await
import kotlinx.coroutines.Job
import kotlinx.coroutines.coroutineScope
import kotlinx.coroutines.joinAll
import kotlinx.coroutines.launch
import org.slf4j.Logger
import xyz.scootaloo.kami.server.standard.getLogger
import xyz.scootaloo.kami.server.service.AppConfig
import xyz.scootaloo.kami.server.service.StageEventEmitter

/**
 * 轻量级组件
 *
 * @author flutterdash@qq.com
 * @since 2022/3/17 20:34
 */
typealias LiteComponent = LightWeightComponent

interface LightWeightComponent {

    val isWorker: Boolean
    val componentName: String

    suspend fun deploy(config: AppConfig): String?
    suspend fun undeploy(): String?

}

object MainDeployer {
    suspend fun deploy(vertx: Vertx, components: List<LiteComponent>) {
        setGlobalVertxRef(vertx)
        clearAndPut(components)

        AutoDeployer.APP_CONFIG = ConfigLoader.loadConfig()
        StageEventEmitter.afterConfigReady(AutoDeployer.APP_CONFIG)

        CompositeFuture.all(
            vertx.deployVerticle(EventLoopDeployer, EventLoopDeployer.deployOptions()),
            vertx.deployVerticle(WorkerDeployer, WorkerDeployer.deployOptions())
        ).await()
    }

    suspend fun undeploy() {
        coroutineScope {
            listOf(
                launch { EventLoopDeployer.autoUnDeploy() },
                launch { WorkerDeployer.autoUnDeploy() }
            ).joinAll()
        }
    }

    private fun clearAndPut(components: List<LiteComponent>) {
        clearAll()
        for (comp in components) {
            if (comp.isWorker) {
                WorkerDeployer.addComponent(comp)
            } else {
                EventLoopDeployer.addComponent(comp)
            }
        }
    }

    private fun clearAll() {
        WorkerDeployer.clearComponents()
        EventLoopDeployer.clearComponents()
    }
}

abstract class AutoDeployer : CoroutineVerticle() {
    private val components: MutableList<LiteComponent> = ArrayList()

    abstract val log: Logger

    abstract val deploymentId: String

    abstract fun deployOptions(): DeploymentOptions

    override suspend fun start() = autoDeploy()
    override suspend fun stop() = autoUnDeploy()

    fun clearComponents() = components.clear()
    fun addComponent(c: LiteComponent) = components.add(c)

    private suspend fun autoDeploy() {
        val jobs = components.map { comp ->
            launch {
                safeCall(comp, "部署") {
                    comp.deploy(APP_CONFIG)
                }
            }
        }
        jobs.joinAll()
    }

    suspend fun autoUnDeploy() {
        val jobs = components.map { comp ->
            launch {
                safeCall(comp, "取消部署") {
                    comp.undeploy()
                }
            }
        }
        jobs.joinAll()
    }

    private suspend fun safeCall(comp: LiteComponent, act: String, block: suspend () -> String?) = try {
        val extraInfo = block()
        var msg = "${comp.componentName}${act}成功"
        if (extraInfo != null) msg += ", $extraInfo"
        log.info(msg)
    } catch (err: Throwable) {
        log.error("${comp.componentName}${act}失败 => ${err.message}", err)
        throw err
    }

    companion object {
        @Volatile var APP_CONFIG: AppConfig = AppConfig()
    }
}

object WorkerDeployer : AutoDeployer() {
    override val log by lazy { getLogger() }
    override val deploymentId get() = deploymentID
    override fun deployOptions() = deploymentOptionsOf(worker = true)

    fun runCoroutineOnContext(block: CoroutineBlock): Job = launch {
        block()
    }
}

object EventLoopDeployer : AutoDeployer() {
    override val log by lazy { getLogger() }
    override val deploymentId get() = deploymentID
    override fun deployOptions() = deploymentOptionsOf(worker = false)

    fun runCoroutineOnContext(block: CoroutineBlock): Job = launch {
        block()
    }
}